﻿/* ================================================================================
<copyright file="LogFactory.cs" company="The Framework Factory, LLC" lastWriteTime="12/05/2012 23:23:47 PM">
  Copyright (c) 2012 The Framework Factory, LLC.  All rights reserved.
</copyright>
=================================================================================== */
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;

using log4net;
using log4net.Appender;
using log4net.Config;
using log4net.Core;
using log4net.Repository;
using log4net.Repository.Hierarchy;

namespace FWF.Logging
{
    /// <summary>
    /// A static component for creating log writers based upon the current component or log name.
    /// See: http://log4net.sourceforge.net/release/1.2.0.30316/doc/manual/introduction.html.
    /// </summary>
    public static class LogFactory
    {
        private static readonly Hierarchy _repository;

        /// <summary>
        /// 
        /// </summary>
        public const string RepositoryName = "FWF";

        /// <summary>
        /// 
        /// </summary>
        public const string ConfigSearchPattern = "FWF*log4net";

        /// <summary>
        /// Initializes the <see cref="LogFactory" /> class.
        /// </summary>
        static LogFactory()
        {
            _repository = LogManager.CreateRepository( RepositoryName ) as Hierarchy;

            var repositoryCanAddAppenders = _repository.Root as IAppenderAttachable;

            if ( repositoryCanAddAppenders != null )
            {
                repositoryCanAddAppenders.AddAppender( new ObserverAppender() );
            }

            string[] listFiles = Directory.GetFiles(
                AppDomain.CurrentDomain.BaseDirectory,
                ConfigSearchPattern,
                SearchOption.TopDirectoryOnly
                );

            if ( listFiles.Length > 0 )
            {
                ConfigureFileBasedLoggingSettings( listFiles[ 0 ] );
            }
            else
            {
                ConfigureDeaultLoggingSettings();
            }

            // State that the logger has been configured to disallow any future updates
            _repository.Configured = true;

            IFWFLog log = Get( typeof( LogFactory ) );

            var builder = new StringBuilder( 1024 );

            using( var stringWriter = new StringWriter( builder ) )
            {
                DumpLogSettings( stringWriter );
            }

            log.Info( builder.ToString() );
        }

        /// <summary>
        /// Configures the file based logging settings.
        /// </summary>
        /// <param name="file">The file.</param>
        private static void ConfigureFileBasedLoggingSettings( string file )
        {
            string configPath = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, file );

            var fileInfo = new FileInfo( configPath );

            XmlConfigurator.ConfigureAndWatch( _repository, fileInfo );

            IFWFLog log = Get( typeof( LogFactory ) );

            log.InfoFormat(
                "Configured logging via the file: {0}",
                file
                );
        }

        /// <summary>
        /// Configures the deault logging settings.
        /// </summary>
        private static void ConfigureDeaultLoggingSettings()
        {
            // By default, set the logging levels and appenders, even if a *.log4net file does not exist

            Assembly thisAssembly = typeof ( LogFactory ).Assembly;

            string fullManifestName = ManifestResource.FindResourceName( thisAssembly, "Default.log4net" );

            if ( string.IsNullOrEmpty( fullManifestName ))
            {
#if DEBUG
                Debug.Fail( "Unable to locate default log4net embedding resource" );
#endif
#if !DEBUG
                return;
#endif
            }

            Stream defaultXmlConfigStream = thisAssembly.GetManifestResourceStream( fullManifestName );

            XmlConfigurator.Configure( _repository, defaultXmlConfigStream );

            IFWFLog log = Get( typeof( LogFactory ) );

            log.ErrorFormat(
                "Unable to locate {0}.log4net config file in directory: {1}.  Will use default logging settings.",
                RepositoryName,
                AppDomain.CurrentDomain.BaseDirectory
                );
        }

        /// <summary>
        /// Gets this instance.
        /// </summary>
        /// <returns></returns>
        public static IFWFLog Get( object itm )
        {
            if ( ReferenceEquals( itm, null ) )
            {
                throw new Exception( "Unable to locate logger - null lookup type" );
            }

            string logName = CreateLogNameFromType( itm.GetType() );

            return GetInternal( logName, false );
        }

        /// <summary>
        /// Gets the specified type.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <returns></returns>
        public static IFWFLog Get( Type type )
        {
            if ( ReferenceEquals( type, null ) )
            {
                throw new Exception( "Unable to locate logger - null lookup type" );
            }

            string logName = CreateLogNameFromType( type );

            return GetInternal( logName, false );
        }

        /// <summary>
        /// Gets the log for the specified category.
        /// </summary>
        /// <param name="custom">The custom.</param>
        /// <returns></returns>
        public static IFWFLog Get( string custom )
        {
            if ( string.IsNullOrEmpty( custom ) )
            {
                throw new Exception( "Unable to locate logger - missing lookup type" );
            }

            return GetInternal( RepositoryName + "." + custom, true );
        }

        /// <summary>
        /// Gets the internal.
        /// </summary>
        /// <param name="logName">Name of the log.</param>
        /// <param name="isCustom">if set to <c>true</c> [is custom].</param>
        /// <returns></returns>
        private static IFWFLog GetInternal( string logName, bool isCustom )
        {
            IFWFLog log = ExtendedLogManager.GetLogger( RepositoryName, logName );

            if ( !isCustom )
            {
#if DEBUG
                /*
                 * It is possible that a request for a logger is for a different component than expected.  Notice
                 * component A is using a logger for component B below.
                 * 
                 * public class A
                 * {
                 *    private static IFWFLog _log = Logger.Get( typeof( B ) );
                 * }
                 * 
                 * In this case, a Warn() entry is used when there is this difference.  Since this logic uses
                 * a StackTrace component, it should only be used when building in DEBUG mode. 
                 * 
                */
                Type callingType = new StackTrace().GetFrame( 2 ).GetMethod().DeclaringType;

                // NOTE: Do not check differences with a generic type.  Will get false positives 
                // with different generic parameters nuances.
                if ( callingType != null && !callingType.IsGenericType )
                {
                    string callingComponentLogName = CreateLogNameFromType( callingType );

                    if ( !callingComponentLogName.Equals( logName, StringComparison.InvariantCultureIgnoreCase ) )
                    {
                        log.WarnFormat(
                            "Invalid log request - calling component, {0}, has different name than logger: {1}.",
                            callingComponentLogName,
                            logName
                            );
                    }
                }
#endif
            }

            return log;
        }

        /// <summary>
        /// Creates the type of the log name from.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <returns></returns>
        private static string CreateLogNameFromType( Type type )
        {
            // If the type is not generic, then the type name is fine
            if ( !type.IsGenericType )
            {
                return type.FullName;
            }

            // NOTE: When dealing with a type with generic parameters, create the log string
            // by appending each generic parameter.
            // Type Name = BusinessEvent<T, U>
            // Log Name  = BusinessEvent.T.U

            // NOTE: Do not use FullName, since it will already contain 
            // the generic parameters with assembly qualified names
            string logName = type.Namespace + "." + type.Name;

            Type[] childTypes = type.GetGenericArguments();

            for ( int i = 0; i < childTypes.Length; i++ )
            {
                // Replace the ugly "`1" in the type name
                logName = logName.Replace( "`" + ( i + 1 ), null );

                logName += "." + childTypes[ i ].Name;
            }

            return logName;
        }

        /// <summary>
        /// 
        /// </summary>
        public static void DumpLogSettings()
        {
            var builder = new StringBuilder( 1024 );

            using ( var stringWriter = new StringWriter( builder ) )
            {
                DumpLogSettings( stringWriter );
            }

            Debug.WriteLine( builder.ToString() );
        }

        /// <summary>
        /// Dumps the log settings.
        /// </summary>
        /// <param name="textWriter">The text writer.</param>
        private static void DumpLogSettings( TextWriter textWriter )
        {
            ILoggerRepository[] listRepositories = LoggerManager.GetAllRepositories();

            textWriter.WriteLine( "log4Net Settings" );
            textWriter.WriteLine( new string( '=', 30 ) );
            textWriter.WriteLine( String.Empty );

            int indent = 0;
            const int IndentStep = 3;

            foreach ( ILoggerRepository loggerRepository in listRepositories )
            {
                textWriter.WriteLine( new string( ' ', indent ) + loggerRepository.Name + " " + loggerRepository.Threshold + ", Configured = " + loggerRepository.Configured );

                indent += IndentStep;

                ILogger[] listLoggers = loggerRepository.GetCurrentLoggers();

                textWriter.WriteLine( new string( ' ', indent ) + "Loggers:" );
                indent += IndentStep;
                foreach ( ILogger logger in listLoggers )
                {
                    var logImpl = logger as Logger;

                    if ( logImpl != null )
                    {
                        textWriter.WriteLine( new string( ' ', indent ) + logger.Name + " {" + logImpl.EffectiveLevel + "}" );
                    }
                    else
                    {
                        textWriter.WriteLine( new string( ' ', indent ) + logger.Name );  
                    }
                }
                indent -= IndentStep;

                IAppender[] listAppenders = loggerRepository.GetAppenders();

                textWriter.WriteLine( new string( ' ', indent ) + "Appenders:" );
                indent += IndentStep;
                foreach ( IAppender appender in listAppenders )
                {
                    if ( !string.IsNullOrEmpty( appender.Name ) )
                    {
                        textWriter.WriteLine( new string( ' ', indent ) + appender.Name );
                    }
                    else
                    {
                        textWriter.WriteLine( new string( ' ', indent ) + appender.GetType().Name );
                    }
                }
                indent -= IndentStep;
                textWriter.WriteLine( String.Empty );

                indent -= IndentStep;
            }

            textWriter.WriteLine( new string( '=', 30 ) );
        }

        /// <summary>
        /// Sets the instance.
        /// </summary>
        /// <param name="name">The name.</param>
        public static void SetInstanceName( string name )
        {
            GlobalContext.Properties[ "instanceName" ] = name;
        }

    }
}
